﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace DDD.Core {

    /// <summary>
    /// グラフィック処理のクラス
    /// </summary>
    /// <remarks>
    /// <see cref="Graphic3D"/> クラスはすべてのグラフィック処理の基本となるクラスです。
    /// GPU（デバイス）を抽象化したものと考えられます。
    /// 構築された <see cref="World">シーン</see> はこの <see cref="Graphic3D"/> クラスの
    ///  <see cref="Graphic3D.Render()"/> メソッドによってレンダリングされます。
    /// このクラスはシングルトン化されていてコンストラクターではなく、<see cref="Graphic3D.GetInstance()"/> メソッドを使用します。
    /// このメソッドは常に同じインスタンスを帰します。
    ///  <para>
    /// <h3>レンダリングの基本</h3>
    /// レンダリングとは「何を」「どこに」「どうやって」描画するのか指定する事です。
    /// レンダリング対象はシーンと呼ばれ、<see cref="World"/> オブジェクトを頂点とする木構造のグラフです。
    /// シーンの詳細については <see cref="World"/> クラスの解説を参照してください。
    /// レンダリング ターゲットはデフォルトのフレーム バッファーか、ユーザー指定のカスタム フレーム バッファーが選択可能です。
    /// 前者はそのままウィンドウに表示され、後者のはテクスチャーとして再利用されるか、
    /// 再度デフォルトのフレーム バッファーに転送されて表示されます。
    /// 後者の技法を一般的にオフライン レンダリングと言います。
    /// オフライン レンダリングの詳細については、<see cref="FrameBuffer"/> クラスの解説を参照してください。
    /// そして「どうやって」に相当するのが、<see cref="RenderingPass"/> イベントです。
    /// <see cref="RenderingPass"/> は <see cref="RenderingPassHandler"/> 型のデリゲートを注入して
    /// DDDの外部からレンダリング処理を定義可能です。
    /// レンダリング パスの詳細については、<see cref="RenderPassHandler"/> クラスの解説を参照してください。
    /// レンダリング パスは複数セット可能です。
    /// </para>
    /// <para>
    /// <h3>フレーム バッファー</h3>
    /// デフォルトのフレーム バッファーは(1)カラー バッファー1つ、(2)デプス バッファー1つ、(3)ステンシル バッファー1つから構成されます。
    /// レンダリング結果は通常そのままウィンドウに表示されます。
    /// <note type="implement">
    /// デフォルトのフレーム バッファーは廃止を検討している。
    /// つまり将来的にはオフライン レンダリング専用となる可能性が高い。
    /// （<see cref="FrameBuffer"/> に転送用 API を追加する必要がある）
    /// これはデフォルトのフレーム バッファーをカプセル化するクラスが存在せず、
    /// 例えばクリア値などをこの <see cref="Graphics3D"/> クラスが持たざるを得ない。
    /// これがエンジン デザイン上非常に気持ち悪い。どう考えても <see cref="FrameBuffer"/> クラスに一本化すべき。
    /// </note>
    /// </para>
    /// <para>
    /// <h3>ビューポート</h3>
    /// ビューポートはウィンドウの左上を原点としピクセル数単位で指定します。
    /// クリップされた描画領域はビューポート サイズに引き延ばされてウィンドウに表示されます。
    /// 通常ビューポートのサイズはウィンドウと同じ物を指定します。
    /// </para>
    /// <para>
    /// <h3>ノード セレクション</h3>
    /// ノード セレクションは <see cref="Func{World, IEnumerable{Node}}"/> 型のデリゲートで、
    /// <see cref="World"/> を受け取り<see cref="Node"/> の列挙子を返すメソッドを受け取ります。
    /// これはシーンに最後の修正の機会を与えます。
    /// レンダリングは（シーン全体ではなく）このノードに対してこの順番で行われます。
    /// 例えばシーン中で半透明物体をカメラから遠い方から順番にレンダリングするには以下のように記述します。
    /// <code>
    /// 省略（後で書く）
    /// </code>
    /// ノード セレクションは省略可能で、そのときはシーン全体を順番不定で描画します。
    /// </para>
    /// </remarks>
    public class Graphics3D {
        static Graphics3D g3d = new Graphics3D ();

        #region Field
        World world;
        FrameBuffer frameBuffer;
        bool colorClearEnabled;
        ClearColor clearColor;
        bool depthClearEnabled;
        float clearDepth;
        Func<World, IEnumerable<Node>> nodeSelection;
        Viewport viewport;
        #endregion

        #region Constructor
        /// <summary>
        /// コンストラクター
        /// </summary>
        /// <remarks>
        /// シングルトン化されているので1回しか呼ばれない。
        /// 初期値を変えたら <see cref="Reset()"/> の方も修正するのを忘れずに。
        /// </remarks>
        internal Graphics3D () {
            this.frameBuffer = null;         // デフォルト
            this.colorClearEnabled = true;
            this.clearColor = new ClearColor (0, 0, 1, 1);
            this.depthClearEnabled = true;
            this.clearDepth = 1.0f;
            this.nodeSelection = null;
            this.viewport = new Viewport (0, 0, 0, 0);
            this.RenderPass = null;
        }
        #endregion


        #region Property

        /// <summary>
        /// レンダリング対象のシーン
        /// </summary>
        /// <remarks>
        /// レンダリング対象のシーンです。ただしシーン全体がそのまま描画されるわけではなく、
        /// ノード セレクションによるノード選択を経たノードが描画対象になります。
        /// </remarks>
        public World World {
            get { return world; }
        }

        /// <summary>
        /// フレーム バッファー
        /// </summary>
        /// <remarks>
        /// 現在デバイスにセットされているフレーム バッファーです。
        /// デフォルトのフレームバッファーの場合は <c>null</c> を返します。
        /// </remarks>
        public FrameBuffer FrameBuffer {
            get { return frameBuffer; }
        }


        /// <summary>
        /// ビューポート
        /// </summary>
        /// <remarks>
        /// 現在のビューポート設定です。
        /// ビューポートはウィンドウの左上を原点にピクセル単位で指定します。
        /// </remarks>
        public Viewport Viewport {
            get { return viewport; }
        }

        /// <summary>
        /// ノード セレクション
        /// </summary>
        /// <remarks>
        /// レンダリングはシーン全体のノードをノード セレクションで取捨選択した結果の列挙順でレンダリングします。
        /// ノード セレクションの実体は引数 <see cref="World"/> を受け取り <see cref="Node"/> の列挙子を返す <see cref="Func{T1,T2}"/> のデリゲートです。
        /// ノード セレクションの詳細については、<see cref="Graphics3D"/> クラスの解説を参照してください。
        /// 初期値は <c>null</c> で、この場合ノードすべてを不定順でレンダリングします。
        /// </remarks>
        public Func<World, IEnumerable<Node>> NodeSelection {
            get { return nodeSelection; }
        }



        /// <summary>
        /// カラー バッファー クリアの有効無効フラグ
        /// </summary>
        /// <remarks>
        /// このフラグはデフォルトのフレーム バッファーがセットされている時（ <see cref="FrameBuffer"/> = <c>null</c> ) に、
        /// カラー バッファーをクリアするかどうかのフラグです。
        /// このプロパティが <c>true</c> の時は値 <see cref="ClearColor"/> でカラー バッファーをクリアします。
        /// <c>false</c> の時は何もしません。
        /// <note type="caution">
        /// このフラグはデフォルトのフレーム バッファーのみに影響します。
        /// ユーザー定義のカスタム フレーム バッファーがアタッチされている場合は無視されます。
        /// このプロパティは廃止予定です。
        /// </note>
        /// </remarks>
        public bool IsColorClearEnabled {
            get { return colorClearEnabled; }
        }

        /// <summary>
        /// デプス バッファーのクリアの有効無効フラグ
        /// </summary>
        /// <remarks>
        /// このフラグはデフォルトのフレーム バッファーがセットされている時（ <see cref="FrameBuffer"/> = <c>null</c> ) に、
        /// デプス バッファーをクリアするかどうかのフラグです。
        /// このプロパティが <c>true</c> の時は値 <see cref="ClearDepth"/> でデプス バッファーをクリアします。
        /// <c>false</c> の時は何もしません。
        /// <note type="caution">
        /// このフラグはデフォルトのフレーム バッファーのみに影響します。
        /// ユーザー定義のカスタム フレーム バッファーがアタッチされている場合は無視されます。
        /// このプロパティは廃止予定です。
        /// </note>
        /// </remarks>
        public bool IsDepthClearEnabled {
            get { return depthClearEnabled; }
        }

        /// <summary>
        /// クリア カラー
        /// </summary>
        /// <remarks>
        /// デフォルトのフレーム バッファーのクリアに使用されるカラー値です。
        /// <note type="implement">
        /// このプロパティは廃止予定です。
        /// </note>
        /// </remarks>
        public ClearColor ClearColor {
            get { return clearColor; }
        }


        /// <summary>
        /// クリア デプス
        /// </summary>
        /// <remarks>
        /// デフォルトのフレーム バッファーのクリアに使用されるデプス値です。
        /// <note type="implement">
        /// このプロパティは廃止予定です。
        /// </note>
        /// </remarks>
        public float ClearDepth {
            get { return clearDepth; }
        }

        #endregion


        #region Events
        /// <summary>
        /// レンダリング パス イベント
        /// </summary>
        /// <remarks>
        /// このイベントは <see cref="RenderPassHandler"/> 型のハンドラーを受け取り、
        /// いわゆるレンダリング パスを設定します。
        /// DDDでは固有のレンダリング パスをエンジン内部で実装せず、
        /// すべて後からユーザーがこのイベントを通して注入します。
        /// レンダリング パスの詳細については、<see cref="Graphics3D"/> クラスの解説を参照してください。
        /// </remarks>
        public event RenderPassHandler RenderPass;
        #endregion


        #region Method

        /// <summary>
        /// シーンの登録
        /// </summary>
        /// <remarks>
        /// レンダリング対象としてシーン <paramref name="wld"/> を登録します。
        /// 登録されたシーンは <see cref="NodeSelection">ノード セレクション</see> で取捨選択されたあと、レンダリングされます。
        /// </remarks>
        /// <param name="wld"><see cref="World"/> オブジェクト</param>
        public void SetWorld (World wld) {
            this.world = wld;
        }

        /// <summary>
        /// フレーム バッファーの登録
        /// </summary>
        /// <remarks>
        /// レンダリング先としてカスタムのフレーム バッファー <paramref name="fbuf"/> を登録します。
        /// <c>null</c> を指定するとデフォルトのフレーム バッファーになります。
        /// </remarks>
        /// <param name="fbuf"><see cref="FrameBuffer"/> オブジェクト</param>
        public void SetFrameBuffer (FrameBuffer fbuf) {
            this.frameBuffer = fbuf;
        }


        /// <summary>
        /// 初期状態にリセット
        /// </summary>
        /// <remarks>
        /// <see cref="Graphics3D"/> オブジェクトをパラメーターを初期値に戻して、
        /// 可能な限りグラフィックデバイスを初期化します。
        /// <note type="implement">
        /// OpenGL のリソースも全部解放して、GPUを初期状態に戻す必要があるが、
        /// 現状でかなり適当。そのうち何とかする。
        /// </note>
        /// </remarks>
        public void Reset () {
            this.frameBuffer = null;
            this.colorClearEnabled = true;
            this.clearColor = new ClearColor (0, 0, 1, 1);
            this.depthClearEnabled = true;
            this.clearDepth = 1.0f;
            this.nodeSelection = null;
            this.viewport = new Viewport (0, 0, 0, 0);
            this.RenderPass = null;
        }


        /// <summary>
        /// レンダリングパス イベントの取得
        /// </summary>
        /// <remarks>
        /// 現在設定済みのレンダリング パス イベントを取得します。
        /// </remarks>
        /// <returns>レンダリングパス</returns>
        public IEnumerable<Delegate> GetRenderPass () {
            return RenderPass.GetInvocationList ();
        }

        /// <summary>
        /// レンダリング パス イベントの削除
        /// </summary>
        /// <remarks>
        /// 設定済みのレンダリング パスをすべて削除します。
        /// 個別に削除するメソッドはありません。
        /// </remarks>
        public void ClearRenderPass () {
            this.RenderPass = null;
        }

        /// <summary>
        /// レンダリング パス イベントの個数の取得
        /// </summary>
        /// <remarks>
        /// 設定済みのレンダリング パスの個数を取得します。
        /// </remarks>
        /// <returns></returns>
        public int GetRenderPassCount () {
            return (RenderPass == null) ? 0 : RenderPass.GetInvocationList ().Length;
        }

        /// <summary>
        /// <see cref="Graphics3D"/> オブジェクトの取得
        /// </summary>
        /// <remarks>
        /// <see cref="Graphic3D"/> オブジェクトはシングルトン化されているので、
        /// このメソッドを使用して唯一のインスタンスを取得します。
        /// 必ず同一のオブジェクトが返ります。
        /// </remarks>
        /// <returns>
        /// 唯一の <see cref="Graphics3D"/> オブジェクト
        /// </returns>
        public static Graphics3D GetInstance () {
            return g3d;
        }

        /// <summary>
        /// ビューポートの設定
        /// </summary>
        /// <remarks>
        /// ビューポートを設定します。ビューポートはウィンドウの左上を原点にピクセル単位で指定します。
        /// <note type="important">
        /// 設定し忘れに注意。
        /// ビューポートの設定はよく忘れます。
        /// </note>
        /// </remarks>
        /// <param name="x">左上のX座標（ピクセル単位）</param>
        /// <param name="y">左上のY座標（ピクセル単位）</param>
        /// <param name="width">幅（ピクセル数）</param>
        /// <param name="height">高さ（ピクセル数）</param>
        /// <exception cref="ArgumentException"><paramref name="x"/> &lt; 0</exception>
        /// <exception cref="ArgumentException"><paramref name="y"/> &lt; 0</exception>
        /// <exception cref="ArgumentException"><paramref name="width"/> &lt; 0</exception>
        /// <exception cref="ArgumentException"><paramref name="height"/> &lt; 0</exception>
        public void SetViewort (int x, int y, int width, int height) {
            if (x < 0) {
                throw new ArgumentException ("X is invalid");
            }
            if (y < 0) {
                throw new ArgumentException ("Y is invalid");
            }
            if (width < 0) {
                throw new ArgumentException ("Width is invalid");
            }
            if (height < 0) {
                throw new ArgumentException ("Height is invalid");
            }

            this.viewport = new Viewport (x, y, width, height);
        }

        /// <summary>
        /// クリア カラーの変更
        /// </summary>
        /// <remarks>
        /// デフォルトのフレーム バッファーの時に、
        /// カラー クリアに使用されるカラー値をチャンネル毎に [0,1] で指定します。
        /// カスタム フレーム バッファーの場合は無視されます。
        /// <note type="implement">
        /// このメソッドは廃止予定です。
        /// </note>
        /// </remarks>
        /// <param name="r">R(赤)</param>
        /// <param name="g">G(緑)</param>
        /// <param name="b">B(青)</param>
        /// <param name="a">A(不透明度)</param>
        /// <exception cref="ArgumentException"><paramref name="r"/> &lt; 0 または <paramref name="r"/> &gt; 1</exception>
        /// <exception cref="ArgumentException"><paramref name="g"/> &lt; 0 または <paramref name="g"/> &gt; 1</exception>
        /// <exception cref="ArgumentException"><paramref name="b"/> &lt; 0 または <paramref name="b"/> &gt; 1</exception>
        /// <exception cref="ArgumentException"><paramref name="a"/> &lt; 0 または <paramref name="a"/> &gt; 1</exception>
        public void SetClearColor (float r, float g, float b, float a) {
            if (r < 0 || r > 1) {
                throw new ArgumentException ("R is invalid");
            }
            if (g < 0 || g > 1) {
                throw new ArgumentException ("G is invalid");
            }
            if (b < 0 || b > 1) {
                throw new ArgumentException ("B is invalid");
            }
            if (a < 0 || a > 1) {
                throw new ArgumentException ("A is invalid");
            }

            this.clearColor = new ClearColor (r, g, b, a);
        }

        /// <summary>
        /// デプス クリア値の変更
        /// </summary>
        /// <remarks>
        /// デフォルトのフレーム バッファーの時に、
        /// デプス クリアに使用されるデプス値を [0,1] で指定します。
        /// 0.0が一番手前で、1.0が一番奥です。
        /// カスタム フレーム バッファーの場合は無視されます。
        /// <note type="implement">
        /// このメソッドは廃止予定です。
        /// </note>
        /// </remarks>
        /// <param name="depth">デプスのクリア値</param>
        /// <exception cref="ArgumentException"><paramref name="depth"/> &lt; 0 または <paramref name="depth"/> &gt; 1</exception>
        public void SetClearDepth (float depth) {
            if (depth < 0 || depth > 1) {
                throw new ArgumentException ("Depth is invalid");
            }
            this.clearDepth = depth;
        }

        /// <summary>
        /// カラー バッファー クリアの有効無効の変更
        /// </summary>
        /// <remarks>
        /// デフォルトのフレーム バッファーの時に、
        /// カラー バッファーをクリアするかどうかのフラグです。
        /// カスタムのフレーム バッファーの場合は無視されます。
        /// <note type="implement">
        /// このメソッドは廃止予定です。
        /// </note>
        /// </remarks>
        /// <param name="enable">有効･無効</param>
        public void SetColorClearEnabled (bool enable) {
            this.colorClearEnabled = enable;
        }

        /// <summary>
        /// デプスバッファーのクリアの有効無効を変更します
        /// </summary>
        /// <param name="enable">有効･無効</param>
        public void SetDepthClearEnabled (bool enable) {
            this.depthClearEnabled = enable;
        }

        /// <summary>
        /// ノードセレクションを変更します
        /// </summary>
        /// <remarks>
        /// 以前のセレクションを破棄し新しいセレクションに変更します。
        /// </remarks>
        /// <param name="select">ノードセレクション</param>
        public void SetNodeSelection (Func<World, IEnumerable<Node>> select) {
            this.nodeSelection = select;
        }

        #endregion




    }
}
